package ru.volgogradetzzz.text 
{
	//--------------------------------------
	// Imports
	//--------------------------------------
	
	import flash.text.engine.BreakOpportunity;
	import flash.text.engine.ContentElement;
	import flash.text.engine.ElementFormat;
	import flash.text.engine.FontDescription;
	import flash.text.engine.FontLookup;
	import flash.text.engine.FontPosture;
	import flash.text.engine.FontWeight;
	import flash.text.engine.GroupElement;
	import flash.text.engine.RenderingMode;
	import flash.text.engine.TextBlock;
	import flash.text.engine.TextElement;
	
	//--------------------------------------
	// Class description
	//--------------------------------------
	
	/**
	 * VFormatableText can contain multiply text lines. Text can set it's width automatically if autoWidth set to true,
	 * and height if autoHeight set to true. In other cases text is wraps or trunkates if string width is greater then
	 * explicitly setted width or string height is greater then explicitly setted height. Text can be formatted.
	 * If range specified in 'formatTextRange' method then format applies only to specified symbols. Text align
	 * doesn't affect text in that case.
	 * 
	 * @author Volgogradetzzz
	 */
	public class VFormatableText extends VMultilineText implements IFTEVText
	{
		private var _group:GroupElement = new GroupElement();
		private var _supportedHtmlTags:Vector.<String> = Vector.<String>(['b', 'br', 'font', 'i']);
		
		//--------------------------------------
		// Constructor
		//--------------------------------------
		
		/**
		 * Creates new instance of VFormatableText.
		 */
		public function VFormatableText() 
		{
			super();
			
			_block = new TextBlock(_group);
		}
		
		//--------------------------------------
		// Public methods
		//--------------------------------------
		
		/**
		 * Sets new format to specified symbol range. Text align doesn't affects text. New format rewrites
		 * old text format values, so if you neet to change only one format parameter, for example, set bold while
		 * keepeng other format values same, you should clone already assigned VTextFormat, change necessary value
		 * and assign that format.
		 * 
		 * @param	beginIndex 	First symbol index.
		 * @param	lastIndex	Last symbol index.
		 * @param	format		Instance of VTextFormat that should be applied.
		 */
		public function formatTextRange(beginIndex:int, lastIndex:int, format:VTextFormat):void
		{
			var elements:Vector.<ContentElement> = new Vector.<ContentElement>();
			
			var fontWeight:String = format.bold ? FontWeight.BOLD : FontWeight.NORMAL;
			var fontStyle:String = format.italic ? FontPosture.ITALIC : FontPosture.NORMAL;
			var embedFont:String = format.embedFont ? FontLookup.EMBEDDED_CFF : FontLookup.DEVICE;
			
			var fontDesc:FontDescription = new FontDescription(format.font, fontWeight, fontStyle, embedFont, RenderingMode.NORMAL);
			var newFormat:ElementFormat = new ElementFormat(fontDesc, format.size, format.color);
			newFormat.breakOpportunity = BreakOpportunity.AUTO;
			
			for (var i:int = beginIndex; i < lastIndex; i++)
			{
				var element:ContentElement = _group.getElementAt(i);
				var textElement:TextElement = new TextElement(element.text, newFormat);
				
				elements.push(textElement);
			}
			
			_group.replaceElements(beginIndex, lastIndex, elements);
			
			if(!_updateLater) createLines();
		}
		
		//--------------------------------------
		// Public properties
		//--------------------------------------
		
		/**
		 * Gets or sets text message for VFormatableText instance. VFormatableText can contain multiply text lines.
		 * If width is not enougth to hold the whole string then string is wraps. If height is not enougth to hold the
		 * whole string then string is trunkates.
		 */
		override public function set text(value:String):void
		{
			_text = value;
			
			var fontWeight:String = _textFormat.bold ? FontWeight.BOLD : FontWeight.NORMAL;
			var fontStyle:String = _textFormat.italic ? FontPosture.ITALIC : FontPosture.NORMAL;
			var embedFont:String = _textFormat.embedFont ? FontLookup.EMBEDDED_CFF : FontLookup.DEVICE;
			
			var fontDesc:FontDescription = new FontDescription(_textFormat.font, fontWeight, fontStyle, embedFont, RenderingMode.NORMAL);
			var format:ElementFormat = new ElementFormat(fontDesc, _textFormat.size, _textFormat.color);
			format.breakOpportunity = BreakOpportunity.AUTO;
			
			var elements:Vector.<ContentElement> = new Vector.<ContentElement>();
			
			var symbols:Array = _text.split('');
			for each(var symbol:String in symbols)
			{
				var textElement:TextElement = new TextElement(symbol, format);
				elements.push(textElement);
			}
			
			_group.setElements(elements);
			
			if(!_updateLater) createLines();
		}
		
		/**
		 * Gets or sets text in html format for VFormatableText instance. VFormatableText can contain multiply text lines.
		 * If width is not enougth to hold the whole string then string is wraps. If height is not enougth to hold
		 * the whole string then string is trunkates.
		 * 
		 * Supported html tags are <b>, <br>, <font> and <i>.
		 * Each tag must be closed or exception will be thrown. For example, <b> tag must be closed with </b> tag.
		 * Note: line break tag can be written in shorthand - <br/>. If you choose <br> tag then it must be
		 * followed by </br> tag. Only spaces allowed between that tags or exception will be thrown.
		 * 
		 * <font> tag supports following attributes:
		 * face - holds name of font.
		 * color - holds color in hexadecimal format (0x000000, for example);
		 * size - size of text in pixels.
		 */
		public function set htmlText(value:String):void
		{
			var newStr:String = value;
			newStr = newStr.replace(/<\s*br\s*\/\s*>/gi, '\n');
			newStr = newStr.replace(/<\s*br\s*>\s*<\s*\/\s*br\s*>/gi, '\n');
			
			setDefaultHtmlTextFormat(newStr);
			
			//get object with tag bame and tag begining index in string
			var tagObj:Object = findFirstTag(newStr);
			
			while (tagObj)
			{
				//get opening tag begin index and end index
				var openingTagPattern:String = '<\\s*' + tagObj.tag + '.*?>';
				var openingPattern:RegExp = new RegExp(openingTagPattern, 'gi');
				var openingTag:String = openingPattern.exec(newStr)[0];
				var openingPatternFirstInd:int = tagObj.beginInd;
				var openingPatternLastInd:int = openingPatternFirstInd + openingTag.length;//openingPattern.lastIndex;
				
				//get closing tag begin index and end index
				var closingTagPattern:String = '<\\s*/\\s*' + tagObj.tag + '\\s*>';
				var closingPattern:RegExp = new RegExp(closingTagPattern, 'gi');
				
				try
				{
				var closingTagInfo:Object = searchForNestedTags(openingPattern, closingPattern, newStr);
				}
				catch (error:Error)
				{
					throw(new Error('Supplied html text is not valid'));
				}
				
				var closingPatternLastInd:int = closingTagInfo.closingPatternLastInd//searchForNestedTags(openingPattern, closingPattern, newStr);
				var closingPatternLength:int = closingTagInfo.closingPatternLength
				var closingPatternFirstInd:int = closingPatternLastInd - closingPatternLength//closingTagPattern.length;
				
				switch(tagObj.tag)
				{
					case 'b':
					{
						for (var i:int = openingPatternLastInd; i < closingPatternFirstInd; i++)
						{
							var textElement:TextElement = _group.getElementAt(i) as TextElement;
							var fontDesc:FontDescription = textElement.elementFormat.fontDescription.clone();
							fontDesc.fontWeight = FontWeight.BOLD;
							var newFormat:ElementFormat = textElement.elementFormat.clone();
							newFormat.fontDescription = fontDesc;
							textElement.elementFormat = newFormat;
						}
						
						break;
					}
					case 'font':
					{
						var modifedAttributes:Array = new Array();
						var attributePattern:RegExp = /(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?/gi;
						var attributes:Array = attributePattern.exec(openingTag);
						
						while (attributes)
						{
							var attributeName:String = attributes[1];
							var attributeValue:String = attributes[2];
							modifedAttributes.push( { attributeName: attributeName, attributeValue:attributeValue } );
							attributes = attributePattern.exec(openingTag);
						}
						
						for (i = openingPatternLastInd; i < closingPatternFirstInd; i++)
						{
							textElement = _group.getElementAt(i) as TextElement;
							fontDesc = textElement.elementFormat.fontDescription.clone();
							newFormat = textElement.elementFormat.clone();
							for each(var obj:Object in modifedAttributes)
							{
								if (obj.attributeName.toLowerCase() == 'face')
								{
									fontDesc.fontName = obj.attributeValue;
								}
								else if (obj.attributeName.toLowerCase() == 'color')
								{
									newFormat.color = uint(obj.attributeValue);
								}
								else if (obj.attributeName.toLowerCase() == 'size')
								{
									newFormat.fontSize = obj.attributeValue;
								}
							}
							newFormat.fontDescription = fontDesc;
							textElement.elementFormat = newFormat;
						}
						
						break;
					}
					case 'i':
					{
						for (i = openingPatternLastInd; i < closingPatternFirstInd; i++)
						{
							textElement = _group.getElementAt(i) as TextElement;
							fontDesc = textElement.elementFormat.fontDescription.clone();
							fontDesc.fontPosture = FontPosture.ITALIC;
							newFormat = textElement.elementFormat.clone();
							newFormat.fontDescription = fontDesc;
							textElement.elementFormat = newFormat;
						}
						
						break;
					}
				}
				
				//remove tags from text
				var symbolsArr:Array = newStr.split('');
				symbolsArr.splice(closingPatternFirstInd, closingPatternLastInd - closingPatternFirstInd), closingPatternFirstInd, closingPatternLastInd;
				symbolsArr.splice(openingPatternFirstInd, openingPatternLastInd - openingPatternFirstInd), openingPatternFirstInd, openingPatternLastInd;
				
				_group.replaceElements(closingPatternFirstInd, closingPatternLastInd, null);
				_group.replaceElements(openingPatternFirstInd, openingPatternLastInd, null);
				newStr = symbolsArr.join('');
				
				//search for tags again
				tagObj = findFirstTag(newStr);
			}
			//assign text without tags;
			_text = newStr;
			
			if(!_updateLater) createLines();
		}
		
		/**
		 * Searches for nested tags and skips them. Returns object with tag length and tag last index in the string.
		 * @param	openingPattern 	RegExp pattern for opening tag.
		 * @param	closingPattern 	RegExp pattern for closing tag.
		 * @param	str				String to search patterns.
		 * @return					Object with tag length and tag last index in the string.
		 */
		private function searchForNestedTags(openingPattern:RegExp, closingPattern:RegExp, str:String):Object
		{
			var obj:Object = { };
			obj.closingPatternLength = closingPattern.exec(str)[0].length;
			obj.closingPatternLastInd = closingPattern.lastIndex;
			
			if (openingPattern.exec(str))
			{
				if (openingPattern.lastIndex < obj.closingPatternLastInd)
				{
					obj = searchForNestedTags(openingPattern, closingPattern, str);
				}
			}
			
			return obj;
		}
		
		/**
		 * Searches for the first appearance of one of supported tags.
		 * @param	str String to search.
		 * @return		Object with tag name and index of thr begining of the tag in the string.
		 */
		private function findFirstTag(str:String):Object
		{
			var firstInd:int = -1;
			var firstTag:String;
			for each(var tag:String in _supportedHtmlTags)
			{
				var pattern:RegExp = new RegExp('<\\s*' + tag, 'i')
				var ind:int = str.search(pattern);
				
				if (firstInd == -1 && ind > -1)
				{
					firstInd = ind;
					firstTag = tag;
				}
				else if(ind != -1 && ind < firstInd)
				{
					firstInd = ind;
					firstTag = tag;
				}
			}
			
			if (firstInd == -1) return null;
			
			return {beginInd: firstInd, tag: firstTag};
		}
		
		/**
		 * Sets default html format, i.e. format that applyed if it was not overwrighted by tags.
		 * @param	str String to apply default format to.
		 */
		private function setDefaultHtmlTextFormat(str:String):void
		{
			var fontWeight:String = FontWeight.NORMAL;
			var fontStyle:String = FontPosture.NORMAL;
			var embedFont:String = _textFormat.embedFont ? FontLookup.EMBEDDED_CFF : FontLookup.DEVICE;
			
			var fontDesc:FontDescription = new FontDescription('Arial', fontWeight, fontStyle, embedFont, RenderingMode.NORMAL);
			var format:ElementFormat = new ElementFormat(fontDesc, 20, 0x000000);
			format.breakOpportunity = BreakOpportunity.AUTO;
			
			var elements:Vector.<ContentElement> = new Vector.<ContentElement>();
			var symbols:Array = str.split('');
			for each(var symbol:String in symbols)
			{
				var textElement:TextElement = new TextElement(symbol, format);
				elements.push(textElement);
			}
			_group.setElements(elements);
		}
	}

}